Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit impact of typeof() in a custom attribute #620

Conversation

MichalStrehovsky
Copy link
Member

@MichalStrehovsky MichalStrehovsky commented Feb 1, 2021

When a type is referenced by a typeof in a custom attribute, don't immediately create a constructed EEType for it.

This is a heuristic - we don't know what the type will be used for. The field/property/constructor parameter that is initialized with the typeof should ideally have a dataflow annotation that describes the intent - the dataflow infrastructure will then make sure we have all that's needed.

I hit this while I was working on #619 - the stubbed out method had a [AsyncStateMachine(typeof(<GetNonFileStreamAsync>d__2))] attribute on it. The code I'm making conditional was making a constructed type for the state machine. Then #618 created a perfect storm that brought a constructed HttpClient object back into the picture and rendered #619 completely ineffective by itself.

When a type is referenced by a typeof in a custom attribute, don't immediately create a constructed EEType for it.

This is a heuristic - we don't know what the type will be used for. The field/property/constructor parameter that is initialized with the `typeof` should ideally have a dataflow annotation that describes the intent - this is then going to root he appropriated structures.
@MichalStrehovsky MichalStrehovsky added the area-NativeAOT-coreclr .NET runtime optimized for ahead of time compilation label Feb 1, 2021
@jkotas
Copy link
Member

jkotas commented Feb 1, 2021

the dataflow infrastructure will then make sure we have all that's needed.

How is the dataflow infrastructure going to do this?

@MichalStrehovsky
Copy link
Member Author

class FooAttribute : Attribute
{
    [DynamicallAccessedMembers(Constructors)]
    public Type Foo;
}

[Foo(Foo = typeof(OtherClass))]
class SomeClass
{
}

class OtherClass
{
}

Dataflow analysis considers this an assignment of OtherClass into Foo. It will make sure we keep all constructors on OtherClass. This also keeps the owning type. Also makes it possible to Activator.CreateInstance whatever we get out of Foo without warnings.

if (typeofType.IsArray
|| (typeofType.HasInstantiation && !typeofType.IsGenericDefinition))
{
// We go for the entire EEType because the reflection stack at runtime will need an EEType anyway
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a good place in the reflection stack that shows the dependency on EEType and that can be linked from here?

It looks odd to me that the reflection stack does not need EETypes for regular types, but it does need them for arrays and generics.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC is this one for arrays:

private static void ValidateElementType(RuntimeTypeInfo elementType, RuntimeTypeHandle typeHandle, bool multiDim, int rank)
{
Debug.Assert(multiDim || rank == 1);
if (elementType.IsByRef)
throw new TypeLoadException(SR.Format(SR.ArgumentException_InvalidArrayElementType, elementType));
// We only permit creating parameterized types if the pay-for-play policy specifically allows them *or* if the result
// type would be an open type.
if (typeHandle.IsNull() && !elementType.ContainsGenericParameters && !(elementType is RuntimeCLSIDTypeInfo))
throw ReflectionCoreExecution.ExecutionDomain.CreateMissingArrayTypeException(elementType, multiDim, rank);
}

And this one for generic types:

protected sealed override RuntimeConstructedGenericTypeInfo Factory(UnificationKey key)
{
bool atLeastOneOpenType = false;
foreach (RuntimeTypeInfo genericTypeArgument in key.GenericTypeArguments)
{
if (genericTypeArgument.IsByRef || genericTypeArgument.IsGenericTypeDefinition)
throw new ArgumentException(SR.Format(SR.ArgumentException_InvalidTypeArgument, genericTypeArgument));
if (genericTypeArgument.ContainsGenericParameters)
atLeastOneOpenType = true;
}
// We only permit creating parameterized types if the pay-for-play policy specifically allows them *or* if the result
// type would be an open type.
if (key.TypeHandle.IsNull() && !atLeastOneOpenType)
throw ReflectionCoreExecution.ExecutionDomain.CreateMissingConstructedGenericTypeException(key.GenericTypeDefinition, key.GenericTypeArguments.CloneTypeArray());
return new RuntimeConstructedGenericTypeInfo(key);
}

(The // We only permit part)

If we just go and delete those lines things will probably still work.

The intent of that code was to blame MakeGenericType/MakeArrayType for reflection failures at runtime instead of having these APIs succeed and getting an exception once we actually want to do something with the type (call static method, activate, etc.).

I think this behavior makes sense for user code because this API is the problem.

It's annoying for framework code because e.g. without the special casing in the compiler we will get an exception when trying to GetCustomAttributeData for the attribute.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought, maybe the compiler doesn't need to do this special casing here. This situation is the same as what we would get when e.g. querying fields:

using System;

ClassWithField.Klass = new ClassWithField() { MyGen = null };

typeof(Gen<>).ToString();

foreach (var f in typeof(ClassWithField).GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public))
{
    Console.WriteLine(f.FieldType.FullName);
}

class ClassWithField
{
    public static ClassWithField Klass;

    public Gen<int> MyGen;
}

class Gen<T> { }

This will fail with:

Unhandled Exception: System.Reflection.MissingMetadataException: 'Gen<System.Int32>' is missing metadata. For more information, please visit http://go.microsoft.com/fwlink/?LinkID=392859
   at System.Reflection.Runtime.TypeInfos.RuntimeConstructedGenericTypeInfo.ConstructedGenericTypeTable.Factory(RuntimeConstructedGenericTypeInfo.UnificationKey) + 0x24b
   at System.Collections.Concurrent.ConcurrentUnifierWKeyed`2.GetOrAdd(K) + 0x23f
   at System.Reflection.Runtime.TypeInfos.RuntimeConstructedGenericTypeInfo.GetRuntimeConstructedGenericTypeInfo(RuntimeTypeInfo, RuntimeTypeInfo[], RuntimeTypeHandle) + 0x6f
   at System.Reflection.Runtime.TypeInfos.RuntimeConstructedGenericTypeInfo.GetRuntimeConstructedGenericTypeInfo(RuntimeTypeInfo, RuntimeTypeInfo[]) + 0x56
   at System.Reflection.Runtime.General.TypeUnifier.GetConstructedGenericType(RuntimeTypeInfo, RuntimeTypeInfo[]) + 0x2a
   at System.Reflection.Runtime.General.TypeResolver.TryResolveTypeSignature(TypeSpecificationHandle, MetadataReader, TypeContext, Exception&) + 0x945
   at System.Reflection.Runtime.General.TypeResolver.TryResolve(Handle, MetadataReader, TypeContext, Exception&) + 0x160
   at System.Reflection.Runtime.General.TypeResolver.Resolve(Handle, MetadataReader, TypeContext) + 0x61
   at System.Reflection.Runtime.FieldInfos.NativeFormat.NativeFormatRuntimeFieldInfo.get_FieldRuntimeType() + 0x93
   at System.Reflection.Runtime.FieldInfos.RuntimeFieldInfo.get_FieldType() + 0x70
   at <Program>$.<Main>$(String[]) + 0x130

The compiler doesn't try to go out of its way to make sure we can access the field type either. Maybe that's fine?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not have opinion, except that consistency and less special cases is good.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, let's see if we can get away without it. We can always restore that behavior.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@MichalStrehovsky
Copy link
Member Author

Of course I wrote a test for this exact scenario.

Looks like the exception message leaves things to be desired, so I need to fix that first to make this change acceptable.

@MichalStrehovsky MichalStrehovsky merged commit 65941ce into dotnet:feature/NativeAOT Feb 5, 2021
@MichalStrehovsky MichalStrehovsky deleted the noConstructedTypeFromAttribute branch February 5, 2021 09:47
MichalStrehovsky added a commit to MichalStrehovsky/runtimelab that referenced this pull request Apr 15, 2021
This is a different take on dotnet#620.

We want to make sure `MakeArrayType`/`MakeGenericType` fails if we don't have an EEType for the constructed type because that's the problematic API and it's good UX to have it fail there (as opposed to failing later because e.g. we can't get a `TypeHandle` in a `CreateInstance` call).

But it's really inconvenient to have this fail e.g. when we're hydrating types for the purpose of comparing method signatures. I've seen a failure yesterday where we couldn't find the `string..ctor(char[])` overload because we didn't have an EEType for `ReadOnlySpan<char>` (in a signature of an unrelated overload).

The AOT compiler could create those EETypes, but it's very wasteful. Instead, I'm relaxing the EEType check so that we only do it from the public APIs. It means people can get to "unusable" `RuntimeTypes` now if they're e.g. field types, or parameter types, but I believe those will mostly be corner cases.

I'm adding tests to make sure all of this is diagnosable.
jkotas pushed a commit that referenced this pull request Apr 15, 2021
This is a different take on #620.

We want to make sure `MakeArrayType`/`MakeGenericType` fails if we don't have an EEType for the constructed type because that's the problematic API and it's good UX to have it fail there (as opposed to failing later because e.g. we can't get a `TypeHandle` in a `CreateInstance` call).

But it's really inconvenient to have this fail e.g. when we're hydrating types for the purpose of comparing method signatures. I've seen a failure yesterday where we couldn't find the `string..ctor(char[])` overload because we didn't have an EEType for `ReadOnlySpan<char>` (in a signature of an unrelated overload).

The AOT compiler could create those EETypes, but it's very wasteful. Instead, I'm relaxing the EEType check so that we only do it from the public APIs. It means people can get to "unusable" `RuntimeTypes` now if they're e.g. field types, or parameter types, but I believe those will mostly be corner cases.

I'm adding tests to make sure all of this is diagnosable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-NativeAOT-coreclr .NET runtime optimized for ahead of time compilation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants